Prisonization Agent-Based Model

Final Project: April 24, 2016

Erin Lane

  • Course ID: CMPLXSYS 530
  • Course Title: Computer Modeling of Complex Systems
  • Term: Winter, 2016

Goal

In this project, I explore the relationship between density of formerly incarcerated individuals within a neighborhood and likelihood that prison culture becomes embedded into a neighborhood’s culture ("prisonization"), using an adaptation of Axelrod’s model of cultural dissemination (1997).

Justification

Through the rise of mass incarceration over the last thirty years, American communities contain more people than ever before who have experienced incarceration. Many low-income and minority communities, disproportionately affected by tough-on-crime legislation, have particularly high concentrations of people returning home from jails and prisons. Prisons, as total institutions, tend to develop their own subcultures as their members are forced to alter their conceptions of self (Goffman 1961). Whether this prison subculture spreads to communities receiving large numbers of former inmates is unknown.

Econometric approaches for estimating effects of incarceration on neighborhoods are generally not feasible due to multidirectional causality. An agent-based modeling approach to understanding this relationship provides bottom-up insight into understanding how influences at the individual level can cause community-level changes in culture. Through the agent-based model I am developing, I can explore the shape of the relationship between density of previously incarcerated individuals in a neighbhorhood and the likelihood that the overall neighborhood culture changes as a result of cultural traits picked up by individuals during incarceration.

Outcome measures

The outcome of interest in this model is the shape of the relationship between initial prisionization levels and equilibrium prisonization levels. A linear or concave relationship between these two levels indicate that there are no compounding effects of high-density incarceration on neighbhorhood culture, whereas a convex relationship indicates that prison culture can spread to the wider community once incarceration reaches some tipping point.

Model outline

1. Space

The space is a two-dimensional grid, where the edges wrap around to form a torus.

2. Agents

The agents are individuals that reside on the grid. Each space on the grid contains an agent. Agents all have a vector of cultural traits, of which prisonized is one.

3. Classes

The model contains three classes:

  • An Agent class related to agents in the model
  • A Features class related to cultural traits possessed by agents
  • A Grid class related to the space in which the agents reside

4. Parameters

The model consists of five parameters

  • prisPct: The initial level of prisonization
  • gridSize: Grid length and width
  • numFeatures: The number of cultural features that make up a culture
  • numTraits: The number of different values each cultural feature can possibly be (expect prisonization, which only has two possible values, 0 or 1)

5. Model summary

The model is initialized by creating a gridSize by gridSize grid, and creating agents to populate the grid. Each agent is randomly assigned a vector indicating that agent's cultural features. prisPct percent of agents in the model are randomly designated as having previously been prisonized as a result of incarceration at the start of the model.

To begin a single step of the model, an agent is picked at random. Next, one of that agent’s four neighbors (north, south, east, and west) is randomly chosen. Each of the agent’s features, including prisonization, is compared to those if its neighbor. If the agent has any features in common with its neighbor, the agent “interacts” with its neighbor, and takes on one of the neighbor’s randomly chosen non-shared features (including prisonization) with some probability equal to the index of similarity between the agent and the neighbor.

Steps are repeated until the model reaches equilibrium. This occurs when all agents have either all features or no features in common with all of their neighbors, thus no further cultural contagion is possible. According to the Axelrod model, this should lead to some number of groups that are culturally homogenous.

Once the model reaches equilibrium, I calculate equilibrium proportion of agents who are prisonized.

6. Parameter sweep

I choose fixed values for gridSize, numFeatures and numTraits, and then sweep through values between 0 and 1 of prisPct, with increments of 0.1. For each value of prisPct, I run the model 100 times. I then collect the model parameters and the equilibrium proportion of prisonized agents and explore the relationship between initial and equilibrium prisonization levels.

Code


In [3]:
# Import necesary modules

#%matplotlib inline
import random
import time
import matplotlib.pyplot as plt
import numpy as np
import csv
import datetime
# import seaborn; seaborn.set()

# Helper function for getting the current time in seconds
millis = lambda: int(round(time.time()*1000))

'''
Features class:
    Static vars:
        * traitCounts - an array with length equal to the feature count. Each
            element holds an integer representing the number of possible traits
            for the corresponding feature.

    Object vars:
        * curTraits - an array with length equal to the feature count. Each
            element holds the current trait value for the corresponding feature.
            For the purposes of this model, curTraits[0] represets the binary
            trait for the prisionization feature.

    Static methods:
        * init(traitCounts, prisPct) - sets the feature count, traitCounts,
            and initial relative prisionization.

    Object methods:
        * randomizeTraits - sets random traits for each feature in the object
        * setTrait - sets the trait of a selected feature.
'''
class Features(object):

    # Initialize the feature count and the trait ranges for those features
    @staticmethod
    def init(traitCounts):
        Features.count = len(traitCounts)
        Features.traitCounts = traitCounts

    def __init__(self):
        # Initialize an empty array with a location for each current trait
        self.curTraits = [0 for i in range(Features.count)]
        self.randomizeTraits()
        self.setTrait(0,0)

    def randomizeTraits(self):
        for i in range(1, Features.count):
            self.curTraits[i] = random.randint(0, self.traitCounts[i]-1)

    def setTrait(self, which, val):
        self.curTraits[which] = val

'''
Agent class:
    Object vars:
        * grid - a reference to the grid in which the agent is located
        * row - row of the grid in which the agent is located
        * col - column of the grid in which the agent is located
        * features - the features object

    Object methods:
        * printTraits - prints to console the current traits for this agent
        * influencePossible - returns a boolean value indicating whether the
            agent could be influenced by any of its neighbors
        * isInfluenced(neighbor) - returns a boolean value indicating whether
            a new interaction with a given neighbor causes the agent to be
            influenced
        * isPrisonized - returns a boolean value indicating whether the agent
            is currently prisionized
        * similarity(neighbor) - returns a similarity index from 0.0 to 1.0
            indicating the agent's cultural similarity to the given neighbor
        * differingTraits(neighbor) - returns an array containing values of the
            features for which the agent and the given neighbor do not share
            the same trait
        * inheritTrait(neighbor) - causes the agent to inherit a randomly
            selected feature trait from the given neighbor
        * executeModel - selects a random neighbor, tests whether the agent
            is influenced, and if it is, causes the agent to inherit a trait
            from that neighbor
'''
class Agent(object):

    def __init__(self, row, col, grid):
        self.grid = grid
        self.row = row
        self.col = col
        self.features = Features()

    def printTraits(self):
        print self.features.curTraits

    def influencePossible(self):
        # Get all neighbors
        r = self.row
        c = self.col

        neighbors = [grid.getAgent((r+1) % self.grid.size, c), grid.getAgent((r-1) % self.grid.size, c), \
            grid.getAgent(r, (c-1) % self.grid.size), grid.getAgent(r, (c+1) % self.grid.size)]

        # Influence is possible if similarity to any neighbor is between 0 and 1
        for i in range(len(neighbors)):
            similarity = self.similarity(neighbors[i])
            if similarity > 0 and similarity < 1:
                return True
        return False

    def isInfluenced(self, neighbor):
        sim = self.similarity(neighbor)
        if sim ==1 or sim ==0:
            return False
        if sim > random.random():
            return True
        else:
            return False

    def isPrisonized(self):
        return True if self.features.curTraits[0] == 1 else False

    def similarity(self, neighbor):
        matchingTraits = 0
        for x in range (Features.count):
            if self.features.curTraits[x] == neighbor.features.curTraits[x]:
                matchingTraits += 1
        return float(matchingTraits) / Features.count

    def differingTraits(self, neighbor):
        diffTraits = []
        for x in range (Features.count):
            if self.features.curTraits[x] != neighbor.features.curTraits[x]:
                diffTraits.append(x)
        return diffTraits

    def inheritTrait(self, neighbor):
        which = random.choice(self.differingTraits(neighbor))
        self.features.curTraits[which] = neighbor.features.curTraits[which]

    def executeModel(self):
        # Pick a neighbor location
        # I changed this to use NSEW neighbors, and to wrap around the grid
        if random.random() > .5:
            row = (self.row + random.choice([1, -1])) % self.grid.size
            col = self.col
        else:
            row = self.row
            col = (self.col + random.choice([1, -1])) % self.grid.size
        # Retrieve neighbor
        neighbor = self.grid.getAgent(row, col)
        if self.isInfluenced(neighbor):
            self.inheritTrait(neighbor)

'''
Grid class:
    Object vars:
        * size - the height / width of the grid
        * agents - a 2D matrix where each element contains an agent

    Object methods:
        * getLocationCount - returns the total number of elements in the
            agents matrix
        * addAgent(row, col) - adds a new agent at the specified matrix location
        * getAgent(row, col) - returns the agent object from the specified
            matrix location
        * getPrisPortion - returns a value from 0.0 to 1.0 representing the
            portion of the total grid population that is currently prisionized
        * isAtEquilibrium - returns a boolean value indicating whether the
            grid object is currently at equilibrium (this occurs when every
            agent in the grid either completly shares the culture of all its
            neighbors or shares no culture with its neighbors)
'''
class Grid(object):

    def __init__(self, size):
        self.agents = [[0 for x in range(size)] for x in range(size)]
        self.size = size

    def getLocationCount(self):
        count = self.size * self.size
        return count

    def addAgent(self, row, col):
        self.agents[row][col] = Agent(row, col, self)

    def getAgent(self, row, col):
        return self.agents[row][col]

    def getPrisPortion(self):
        prisPop = 0
        for x in range(self.size):
            for y in range(self.size):
                if self.getAgent(x, y).isPrisonized():
                    prisPop += 1
        return float(prisPop) / self.getLocationCount()

    def isAtEquilibrium(self):
        for x in range(self.size):
            for y in range(self.size):
                if(self.getAgent(x, y).influencePossible()):
                    return False
        return True

def printSimilarities():
    for row in range(grid.size):
        for col in range(grid.size):
            agent = grid.getAgent(row, col)
            print "(" + str(row) + ", " + str(col) + ") " + str(agent.features.curTraits)
            print agent.similarity(grid.getAgent(row, (col+1) % grid.size))
            print agent.similarity(grid.getAgent((row+1) % grid.size , col))

def printFeatures():
    for row in range(grid.size):
        for col in range(grid.size):
            agent = grid.getAgent(row, col)
            print str(row) + ", " + str(col) + " " + str(agent.features.curTraits)


# Parameters
gridSize = 10
numFeatures = 10
numTraits = 8
intervals = 10
loops = 100


# Set up output
with open('pris_output.csv', 'wb') as csvfile:
    csvoutput = csv.writer(csvfile, delimiter=',',
                            quotechar = '|', quoting=csv.QUOTE_MINIMAL)
    csvoutput.writerow(["date", "time", "gridSize", "numFeatures", "numTraits", "prisPct", "endingPrisPortion"])
    # runHistory = None
    
    # Run the model
    stepSize = 100/intervals
    for pct in range(0,101,stepSize):
        print pct
        for loop in range(loops):
            prisPct = pct/100.00
            traitCounts = [2]
            for x in range(numFeatures):
                traitCounts.append(numTraits)

            '''
            Initialize the features class with the number of different traits for each of
            the features and the initial relative prisionization rate.
            '''
            Features.init(traitCounts)

            # Initialize the grid size, add agents
            grid = Grid(gridSize)
            for x in range(grid.size):
                for y in range(grid.size):
                    grid.addAgent(x, y)

            # Assign initial prisionization
            x = round(prisPct*grid.getLocationCount())
            loopCount = 0
            while x > 0:
                loopCount += 1
                row = random.randint(0,grid.size-1)
                col = random.randint(0,grid.size-1)
                agent = grid.getAgent(row, col)
                if agent.isPrisonized() == False:
                    agent.features.setTrait(0,1)
                    x -= 1

            # Run the model
            iteration = 0
            running = True
            startTime = millis()
            while running:
                # Select a random agent for this model step, then execute the model
                thisAgent = grid.getAgent(random.randint(0, (grid.size - 1)), random.randint(0, (grid.size - 1)))
                thisAgent.executeModel()
                iteration += 1
                # Only check for equilibrium once in a while to save time
                if iteration % 10 == 0:
                    if grid.isAtEquilibrium():
                        running = False

            # Report model results
            csvoutput.writerow([datetime.datetime.now().date(), datetime.datetime.now().time(), gridSize, numFeatures, numTraits, prisPct, grid.getPrisPortion()])


0
10
20
30
40
50
60
70
80
90
100

Analysis of Model Results

Output

The model writes a CSV file that contains, for each model run:

  • Date of model run
  • Time of model run
  • Model iteration number
  • gridSize
  • numFeatures
  • numTraits
  • Starting prisonization level
  • Ending prisonization level

Ending prisonization level means and standard devaiations by starting prisonization level are calculated below:


In [44]:
import numpy as np
import pandas as pd
from pandas import *

data = pd.read_csv('/Users/erinlane/Box Sync/CSCS_530/prisonization_github/pris_output.csv')
df = DataFrame(data, columns = ['prisPct', 'endingPrisPortion'])

# Create table with means and standard deviations
mean_endPris = df.groupby(["prisPct"])["endingPrisPortion"].mean()
std_endPris = df.groupby(["prisPct"])["endingPrisPortion"].std()
endPris = pandas.concat((mean_endPris, std_endPris), axis=1)
endPris.columns = ["mean", "std"]
print endPris


         mean       std
prisPct                
0.0      0.00  0.000000
0.1      0.10  0.301511
0.2      0.19  0.394277
0.3      0.22  0.416333
0.4      0.28  0.451261
0.5      0.50  0.502519
0.6      0.61  0.490207
0.7      0.69  0.464823
0.8      0.86  0.348735
0.9      0.92  0.272660
1.0      1.00  0.000000

Hypothesis testing

The null hypothesis in the model is that the expected ending level of prisonization is equal to the level of prisonization at the outset. This certainly appears to be the case based on the model output listed above. To formally test this, I import the CSV with model results into Stata, regress endingPrisPortion on prisPct, and test that the coefficient on prisPct = 1.

I fail to reject the null hypothesis, indicating a linear relationship between initial prisonization levels and ending prisonization levels.

. import delimited "C:\Users\erlane\Box Sync\CSCS_530\prisonization_github\pris_output.csv", clear
(7 vars, 1,100 obs)

. regress endingprisportion prispct

      Source |       SS           df       MS      Number of obs   =     1,100
-------------+----------------------------------   F(1, 1098)      =    869.78
       Model |  121.485091         1  121.485091   Prob > F        =    0.0000
    Residual |  153.361273     1,098   .13967329   R-squared       =    0.4420
-------------+----------------------------------   Adj R-squared   =    0.4415
       Total |  274.846364     1,099  .250087683   Root MSE        =    .37373

------------------------------------------------------------------------------
endingpris~n |      Coef.   Std. Err.      t    P>|t|     [95% Conf. Interval]
-------------+----------------------------------------------------------------
     prispct |   1.050909   .0356337    29.49   0.000     .9809914    1.120827
       _cons |  -.0372727   .0210812    -1.77   0.077    -.0786366    .0040912
------------------------------------------------------------------------------

. test _b[prispct]=1

 ( 1)  prispct = 1

       F(  1,  1098) =    2.04
            Prob > F =    0.1534

Next steps

I was surprised to find that the model resulted in a linear relationship between initial prisonization level and the ending prisonization level, as were others familiar with the Axelrod model who came to my CSAAW presentation.

One possible issue with my model is that it only allows for a maximum of two cultural zones at equilibrium. This is because there are only two possible values for prisonization, 0 and 1. With the setup of the Axelrod model, the maximum number of cultural zones at equilibrium is limited by the number of possible values for the cultural feature with the least number of possible values, because at equilibrium, all agents within a zone must be totally idential or totally different from all of its neighbors. Thus, there can only be at maximum one homogenous prisonized zone and one homogenous non-prisonized zone at equilibrium. A next step might be to think of prisonization as having more than two possible outcomes, and then seeing if this makes a difference in the results. It seems unlikely that this will change the overall results, but worth investigating nonetheless.

Another direction I'd like to take is to try using a network setup rather than a grid setup. This will allow me to create agents with different levels of influence over their networks based on the number and strengths of social ties at model initialization.